'use strict'

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

const divOverlayWrapper = document.getElementById('overlay-wrapper')

const inputServerIp = document.getElementById('input-server-ip')
const inputServerPort = document.getElementById('input-server-port')
const inputName = document.getElementById('input-name')

const checkboxShowName = document.getElementById('showName')
const checkboxShowGrid = document.getElementById('showGrid')
const checkboxShowChat = document.getElementById('showChat')
const checkboxDarkTheme = document.getElementById('darkTheme')
const checkboxManualScale = document.getElementById('manualScale')

const buttonPlay = document.getElementById('button-play')

const BALL_TYPE_FOOD = 0
const BALL_TYPE_VIRUS = 1
const BALL_TYPE_SPORE = 2
const BALL_TYPE_CELL = 3

const IN_PACKET_TYPE_START_GAME_OK = 1
const IN_PACKET_TYPE_UPDATE_RANKING = 2
const IN_PACKET_TYPE_UPDATE_VISIBLE_DATA = 3
const IN_PACKET_TYPE_CHAT_MESSAGE = 4
const IN_PACKET_TYPE_CHAT_DEAD = 5

const OUT_PACKET_TYPE_START_GAME = 1
const OUT_PACKET_TYPE_MOUSE_MOVE = 2
const OUT_PACKET_TYPE_VOMIT_ON = 3
const OUT_PACKET_TYPE_VOMIT_OFF = 4
const OUT_PACKET_TYPE_SPLIT = 5
const OUT_PACKET_TYPE_CHAT_MESSAGE = 6

const ValidIpAddressRegex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/
const ValidHostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/
const ValidPortRegex = /^[0-9]$|(^[1-9][0-9]$)|(^[1-9][0-9][0-9]$)|(^[1-9][0-9][0-9][0-9]$)|(^[1-6][0-5][0-5][0-3][0-5]$)/

let ws = null

// ready connect playing dead disconnect
let state = 'ready'

const gameMap = {
  left: 0,
  top: 0,
  right: 0,
  bottom: 0,
  width: 0,
  height: 0,
  centerX: 0,
  centerY: 0,
}

const visibleData = {
  totalMass: 0,
  view: {
    scale: 2,
    centerX: 0,
    centerY: 0,
    width: 0,
    height: 0,
  },
  balls: []
}

let lastLoopTime = Date.now()

let fps = 0
let lastUpdateFpsTime = 0

let ping = 0
let lastUpdatePingTime = 0

let flow = 0

// util
function worldToScreen(x, y) {
  const view = visibleData.view
  return {
    x: (canvas.width / 2) + (x - view.centerX) * view.scale,
    y: (canvas.height / 2) + (y - view.centerY) * view.scale
  }
}

function screenToWorld(x, y) {
  const view = visibleData.view
  return {
    x: view.centerX + (x - (canvas.width / 2)) / view.scale,
    y: view.centerY + (y - (canvas.height / 2)) / view.scale
  }
}

function getRandomName() {
  let result = '0x'
  let chars = '0123456789ABCDEF'
  for(let i = 0; i < 6; i++) {
    result += chars.charAt(Math.floor(Math.random() * chars.length))
  }
  return result
}

function drawGrid_old() {
  const view = visibleData.view
	let gridSize = 50
	ctx.beginPath()
	let xOff = (canvas.width / 2 - view.centerX * view.scale) % gridSize
	let yOff = (canvas.height / 2 - view.centerY * view.scale) % gridSize
	for(let i = 0; i < canvas.width / gridSize + 1; i++) {
		let x = i * gridSize + xOff
		ctx.moveTo(x, 0)
		ctx.lineTo(x, canvas.height)
	}
	for(let i = 0; i < canvas.height / gridSize + 1; i++) {
		let y = i * gridSize + yOff
		ctx.moveTo(0, y)
		ctx.lineTo(canvas.width, y)
	}
	ctx.lineWidth = 1
	ctx.strokeStyle = checkboxDarkTheme.checked ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)'
	ctx.stroke()
}

function drawGrid() {
  const view = visibleData.view
  const lines = 10
  const gridCellWidth = gameMap.width / (lines - 1)
  const gridCellHeight = gameMap.height / (lines - 1)
  const leftTop = worldToScreen(gameMap.left, gameMap.top)
  const rightBottom = worldToScreen(gameMap.right, gameMap.bottom)
  ctx.beginPath()
  for(let i = 0; i < lines; i++) {
    let pos = worldToScreen(i * gridCellWidth, i * gridCellHeight)
    ctx.moveTo(pos.x, leftTop.y)
    ctx.lineTo(pos.x, rightBottom.y)
    ctx.moveTo(leftTop.x, pos.y)
    ctx.lineTo(rightBottom.x, pos.y)
  }
	ctx.closePath()
  ctx.lineWidth = view.scale
  ctx.strokeStyle = checkboxDarkTheme.checked ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)'
	ctx.stroke()
}

function drawStatus() {
	ctx.font = '24px Roboto'
	ctx.fillStyle = 'green'
	ctx.fillText('fps:' + fps + '  ping:' + ping + 'ms' + '  flow:' + flow + 'kb/s' + '  mass:' + visibleData.totalMass, 10, 30)
}

function drawFood(food) {
  if(!food._vertices || food._oldRadius !== food.radius) {
		const vertexCount = Math.floor(food.radius * 2.5) + 5
		food._vertices = []
		food._oldRadius = food.radius
		let rotation = Math.random() * Math.PI
		for(let i = 0; i <= vertexCount; i++) {
			let angle = i / (vertexCount / 2) * Math.PI + rotation
			food._vertices.push({
				x: food.x + food.radius * Math.cos(angle),
				y: food.y + food.radius * Math.sin(angle)
			})
		}
  }
	ctx.beginPath()
	for(let { x, y } of food._vertices) {
    let pos = worldToScreen(x, y)
		ctx.lineTo(pos.x, pos.y)
	}
	ctx.closePath()
	ctx.fillStyle = food.color
	ctx.fill()
}

function drawVirus(virus) {
	const spikeCount = Math.round(virus.radius * 1.2) * 6

  if(!virus._spikes || virus._oldRadius != virus.radius) {
		virus._spikes = []
		for(let i = 0; i <= spikeCount * 2; i++) {
			let angle = i / spikeCount * Math.PI
			let extra = i % 2 === 0 ? -0.5 : -0.1
			virus._spikes.push({
				x: virus.x + (virus.radius + extra) * Math.cos(angle),
				y: virus.y + (virus.radius + extra) * Math.sin(angle)
			})
		}
		virus._oldRadius = virus.radius
	}

	ctx.beginPath()
	for(let { x, y } of virus._spikes) {
    let pos = worldToScreen(x, y)
		ctx.lineTo(pos.x, pos.y)
	}
	ctx.closePath()
	ctx.fillStyle = virus.color
	ctx.fill()

	ctx.lineWidth = 0.5 * visibleData.view.scale
	ctx.strokeStyle = virus.color
	ctx.stroke()
}

function drawSpore(spore) {
  let pos = worldToScreen(spore.x, spore.y)
  ctx.beginPath()
  ctx.arc(pos.x, pos.y, spore.radius * visibleData.view.scale, 0, Math.PI * 2)
  ctx.closePath()
  ctx.fillStyle = spore.color
  ctx.fill()
}

function drawCell(cell) {
  let pos = worldToScreen(cell.x, cell.y)
  ctx.beginPath()
  ctx.arc(pos.x, pos.y, cell.radius * visibleData.view.scale, 0, Math.PI * 2)
  ctx.closePath()
  ctx.fillStyle = cell.color
  ctx.fill()
  if(checkboxShowName.checked && cell.name) {
    ctx.font = "14px Roboto"
    ctx.textAlign = "center"
    ctx.fillStyle = checkboxDarkTheme.checked ? "#fff" : "#000"
    ctx.fillText(cell.name, pos.x, pos.y + 8)
  }
}

// draw
function draw() {
  if(state !== 'playing' && state !== 'dead') {
    return
  }

  ctx.fillStyle = checkboxDarkTheme.checked ? 'rgb(28, 28, 34)' : 'rgb(255, 255, 255)'
	ctx.fillRect(0, 0, canvas.width, canvas.height)

  if(checkboxShowGrid.checked) {
    drawGrid()
  }
  
  const balls = visibleData.balls
  balls.sort((a, b) => {
    if(a.mass > b.mass) {
      return 1
    } else if(a.mass < b.mass) {
      return -1
    }
    return 0
  }).forEach(ball => {
    if(ball.type === BALL_TYPE_FOOD) {
      drawFood(ball)
    } else if(ball.type === BALL_TYPE_VIRUS) {
      drawVirus(ball)
    } else if(ball.type === BALL_TYPE_SPORE) {
      drawSpore(ball)
    } else if(ball.type === BALL_TYPE_CELL) {
      drawCell(ball)
    }
  })

  drawStatus()
}

function gameloop() {
  let currentTime = Date.now()
  let elapsedMilli = currentTime - lastLoopTime
  lastLoopTime = currentTime
  if(currentTime - lastUpdateFpsTime >= 1000) {
    lastUpdateFpsTime = currentTime
    fps = Math.floor(1000.0 / elapsedMilli)
  }
  draw()
  requestAnimationFrame(gameloop)
}

// packets sent from the server

function inPacket_StartGameOK(packet) {
  let offset = 1
  
  gameMap.left = packet.getFloat32(offset)
  offset += 4

  gameMap.top = packet.getFloat32(offset)
  offset += 4

  gameMap.right = packet.getFloat32(offset)
  offset += 4

  gameMap.bottom = packet.getFloat32(offset)

  gameMap.width = gameMap.right - gameMap.left
  gameMap.height = gameMap.bottom - gameMap.top
  gameMap.centerX = gameMap.left + gameMap.width / 2
  gameMap.centerY = gameMap.top + gameMap.height / 2

  divOverlayWrapper.style.display = 'none'
  state = 'playing'
}

function inPacket_UpdateRanking(packet) {
  
}

let lastUpdateVisibleDataTime = Date.now()
function inPacket_UpdateVisibleData(packet) {
  let currentTime = Date.now()
  if(currentTime - lastUpdatePingTime > 1000) {
    ping = currentTime - lastUpdateVisibleDataTime
    flow = Math.floor((packet.byteLength * (1000 / ping)) / 1024)
    lastUpdatePingTime = currentTime
  }
  lastUpdateVisibleDataTime = currentTime
  
  let offset = 1

  visibleData.totalMass = Math.round(packet.getFloat32(offset))
  offset += 4

  if(checkboxManualScale.checked) {
    packet.getFloat32(offset)
  } else {
    visibleData.view.scale = packet.getFloat32(offset)
  }
  offset += 4

  visibleData.view.centerX = packet.getFloat32(offset)
  offset += 4

  visibleData.view.centerY = packet.getFloat32(offset)
  offset += 4
  
  visibleData.view.width = packet.getFloat32(offset)
  offset += 4
  
  visibleData.view.height = packet.getFloat32(offset)
  offset += 4

  let ballCount = packet.getInt32(offset)
  offset += 4

  let temp = []
  for(let i = 0; i < ballCount; i++) {
    let ball = {}
    ball.type = packet.getInt8(offset++)

    ball.id = packet.getUint32(offset)
    offset += 4

    ball.x = packet.getFloat32(offset)
    offset += 4

    ball.y = packet.getFloat32(offset)
    offset += 4

    ball.mass = packet.getFloat32(offset)
    offset += 4

    ball.radius = packet.getFloat32(offset)
    offset += 4

    let hexColor = packet.getUint32(offset)
    offset += 4

    ball.color = `rgb(${(hexColor >> 16) & 0xff}, ${(hexColor >> 8) & 0xff}, ${hexColor & 0xff})`

    if(ball.type == BALL_TYPE_CELL) {
      // todo: read cell name string
    }
  
    for(let j = 0; j < visibleData.balls.length; j++) {
      if(ball.id === visibleData.balls[j].id && ball.type === visibleData.balls[j].type) {
        visibleData.balls[j].x = ball.x
        visibleData.balls[j].y = ball.y
        visibleData.balls[j].mass = ball.mass
        visibleData.balls[j].radius = ball.radius
        visibleData.balls[j].color = ball.color
        ball = visibleData.balls[j]
        break
      }
    }

    temp.push(ball)
  }

  visibleData.balls = temp
}

function inPacket_ChatMessage(packet) {
  
}

function inPacket_Dead() {
  
}

function onInPacket(packet) {
  switch(packet.getInt8()) {
  case IN_PACKET_TYPE_START_GAME_OK:
    inPacket_StartGameOK(packet)
    break
  case IN_PACKET_TYPE_UPDATE_RANKING:
    inPacket_UpdateRanking(packet)
    break
  case IN_PACKET_TYPE_UPDATE_VISIBLE_DATA:
    inPacket_UpdateVisibleData(packet)
    break
  case IN_PACKET_TYPE_CHAT_MESSAGE:
    inPacket_ChatMessage(packet)
    break
  case IN_PACKET_TYPE_CHAT_DEAD:
    inPacket_Dead()
    break
  }
}


// packets sent to the server

function outPacket_StartGame(name) {
  let offset = 0
  let data = new DataView(new ArrayBuffer(1 + 16))
  data.setInt8(offset++, OUT_PACKET_TYPE_START_GAME)
  for(let i = 0; i < Math.min(name.length, 16); i++) {
    data.setInt8(offset++, name.charCodeAt(i))
  }
  ws.send(data)
}

function outPacket_MoveMouse(x, y) {
  let data = new DataView(new ArrayBuffer(1 + 4 + 4))
  data.setInt8(0, OUT_PACKET_TYPE_MOUSE_MOVE)
  data.setFloat32(1, x)
  data.setFloat32(5, y)
  ws.send(data)
}

function outPacket_Vomit(on) {
  let data = new DataView(new ArrayBuffer(1))
  if(on) {
    data.setInt8(0, OUT_PACKET_TYPE_VOMIT_ON)
  } else {
    data.setInt8(0, OUT_PACKET_TYPE_VOMIT_OFF)
  }
  ws.send(data)
}

function outPacket_Split() {
  let data = new DataView(new ArrayBuffer(1))
  data.setInt8(0, OUT_PACKET_TYPE_SPLIT)
  ws.send(data)
}

function outPacket_ChatMessage(message) {
  let data = new DataView(new ArrayBuffer(1 + message.length))
  data.setInt8(0, OUT_PACKET_TYPE_CHAT_MESSAGE)
  for(let i = 0; i < message.length; i++) {
    data.setInt8(1 + i, message.charCodeAt(i))
  }
  ws.send(data)
}

// websocket events
function onWsOpen(e) {
  state = 'connect'
  buttonPlay.disabled = true
  buttonPlay.style.backgroundColor = 'gray'
  outPacket_StartGame(inputName.value || getRandomName())
}

function onWsMessage(e) {
  onInPacket(new DataView(e.data))
}

function onWsClose(e) {
  state = 'disconnect'
}

function onWsError(e) {
  state = 'disconnect'
}

// window events

function onStartGame(e) {
  let serverIp = inputServerIp.value.trim()
  let serverPort = inputServerPort.value.trim()
  if(!ValidHostnameRegex.test(serverIp) && !ValidIpAddressRegex.test(serverIp)) {
    alert('地址格式错误!')
    return
  }
  if(!ValidPortRegex.test(serverPort)) {
    alert('端口格式错误!')
    return
  }
  let url = `ws://${serverIp}:${serverPort}/ws`
  console.log(`websocket connection to ${url}`)
  ws = new WebSocket(url)
  ws.binaryType = 'arraybuffer'
  ws.onopen = onWsOpen
  ws.onmessage = onWsMessage
  ws.onclose = onWsClose
  ws.onerror = onWsError
}

function onMoveMouse(e) {
  if(state !== 'playing') {
    return
  }
  let {
    x, y
  } = screenToWorld(e.x, e.y)
  outPacket_MoveMouse(x, y)
}

function onMouseWheel(e) {
  if(!checkboxManualScale.checked) {
    return
  }
  if(e.wheelDelta > 0) {
    if(visibleData.view.scale > 10) {
      return
    }
    visibleData.view.scale += 0.5
  } else {
    if(visibleData.view.scale < 1) {
      return
    }
    visibleData.view.scale -= 0.5
  }
}

function onKeyDown(e) {
  if(state !== 'playing') {
    return
  }
  switch(e.key) {
  case 'w':
  case 'W':
    outPacket_Vomit(true)
    break
  case 'e':
  case 'E':
    outPacket_Split()
    break
  case 'Enter':
    break
  }
}

function onKeyUp(e) {
  if(state !== 'playing') {
    return
  }
  if(e.key === 'w' || e.key === 'W') {
    outPacket_Vomit(false)
  }
}

function onWindowResize() {
  canvas.width = window.innerWidth
  canvas.height = window.innerHeight
}

function setup() {
  canvas.width = window.innerWidth
  canvas.height = window.innerHeight

  buttonPlay.onclick = onStartGame

  window.addEventListener('mousemove', onMoveMouse)
  window.addEventListener('mousewheel', onMouseWheel)
  window.addEventListener('keydown', onKeyDown)
  window.addEventListener('keyup', onKeyUp)
  window.addEventListener('resize', onWindowResize)

  gameloop()
}

window.onload = setup
